package test;

import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;

import no.ntnu.fp.net.cl.ClException;
import no.ntnu.fp.net.cl.KtnDatagram;
import no.ntnu.fp.net.cl.KtnDatagram.Flag;
import no.ntnu.fp.net.co.Connection;
//import no.ntnu.fp.net.co.AbstractConnection.State;
//import no.ntnu.fp.net.co.AbstractConnection.State;

public class NaturalJoin extends no.ntnu.fp.net.co.AbstractConnection{
	private static final int MAX_TRIES = 4;
	
	
	private HashMap<Integer, Boolean> usedPorts = new HashMap<Integer, Boolean>();	
	
	public NaturalJoin(int myPort) {
		
		/*
		 * Initialiserer Connection
		 */
		usedPorts.put(myPort, true);
		this.myPort = myPort;
		this.myAddress = getIPv4Address();
	}
	
	public void connect(InetAddress remoteAddress, int remotePort)
	throws IOException, SocketTimeoutException {
		
		if (state != State.CLOSED) {
			System.out.println("Client can not initiate connection while not closed");
			return;
		}
		
		this.remoteAddress = remoteAddress.getHostAddress();
		this.remotePort = remotePort;
		
		KtnDatagram syn = constructInternalPacket(Flag.SYN);
		KtnDatagram synack = null;
		state = State.SYN_SENT;
		do {
			try {
				simplySendPacket(syn);
				synack = receiveAck();
			} catch (ClException e) { }
		} while (!isValid(synack));
		
		if (isValid(synack)) {
			// TODO: Implement resend
			this.remotePort = synack.getSrc_port();
			sendAck(synack, false);
			state = State.ESTABLISHED;
		} 
		else {
			System.out.println("CLIENT: Conncetion failed");	
		}
		}

	@Override
	public Connection accept() throws IOException, SocketTimeoutException {
			
			if (state != State.CLOSED) {
				System.out.println("Server can not accept connection while not closed");
				return null;
			}
			
			state = State.LISTEN;
			int tries = 0;
			
			KtnDatagram syn = null;
			do {
				syn = receivePacket(true);
			} while (!isValid(syn));
			
			state = State.CLOSED;
			
			NaturalJoin connection = new NaturalJoin(newPortNumber());
			
			connection.remoteAddress = syn.getSrc_addr();
			connection.remotePort = syn.getSrc_port();
			connection.state = State.SYN_RCVD;
			
			KtnDatagram finalAck = null;
			do {
				connection.sendAck(syn, true);
				finalAck = connection.receiveAck();
			} while (!connection.isValid(finalAck) && tries++ <= MAX_TRIES);
			
			if (connection.isValid(finalAck)) {
				System.out.println("SERVER: Connection established");
				connection.state = State.ESTABLISHED;
				return connection;
			}
			
			return null;
		}

	@Override
	public void send(String msg) throws ConnectException, IOException {
		if(state!=State.ESTABLISHED){
			throw new IllegalStateException("Can not send packet if state not established."
					+ "\nCurrent state is: " + state);
		}
		KtnDatagram packetToSend = constructDataPacket(msg), ackFromPacket = null;
		int tries = 10;
		try{
			while(!isValid(ackFromPacket) && tries > 0){
			
			ackFromPacket = sendDataPacketWithRetransmit(packetToSend);
			
			tries--;
			}
		}catch(ConnectException e){
			System.out.println("Could not send packet. Connection failed.");
		}catch(IOException e){
			e.printStackTrace();
		}
		if(isValid(ackFromPacket)){
			lastValidPacketReceived = ackFromPacket;
			
			lastDataPacketSent = packetToSend;
		}

		System.out.println(msg);
	
	}

	@Override
	public String receive() throws ConnectException, IOException {
		if(state!= State.ESTABLISHED){
			throw new IllegalStateException("Can't receive because of invalid state."+
					"\nCurrent state is" + state);
		}
		
		String receivedPacketPayload = null;

		
		KtnDatagram receivedPacket = null;// = receivePacket(false); 
		
		try{
			receivedPacket = receivePacket(false);
			//System.out.println(receivedPacket.toString());
		}catch(EOFException e){
			System.out.println("Server closing down");
			close();
			return null;
			//throw e;
			
		}catch(IOException e){}
		/*
		while(!isValid(receivedPacket)){
			System.out.println("ReceivedPacket not valid. Method receive()");
			return null;
		}*/
		/*
		if(receivedPacket.getFlag() == Flag.FIN){
			System.out.println("Received packet with FIN-flag");
			sendAck(disconnectRequest, false);
			close();
			return receivedPacket.getPayload().toString();
			
		}
		*/
		lastValidPacketReceived = receivedPacket;
		sendAck(receivedPacket, false);
		receivedPacketPayload = receivedPacket.getPayload().toString();
		System.out.println("Received: " + receivedPacketPayload);
		return receive();
	}


	@Override
public void close() throws IOException {
		
		if (state != State.ESTABLISHED) {
			System.out.println("Can not close a connection when not established");
			return;
		}
		
		// Client side
		if (disconnectRequest == null) {
			
			KtnDatagram fin = constructInternalPacket(Flag.FIN);
			KtnDatagram ack = null;
			int tries = 0;
			do {
				try {
					state = State.ESTABLISHED;
					simplySendPacket(fin);
					
					state = State.FIN_WAIT_1;
					ack = receiveAck();
				} catch (ClException e) {}
			} while (!isValid(ack) /*&& (tries++ < MAX_TRIES)*/);
			
			if (!isValid(ack)) {
				System.out.println("CLIENT: ACK not received");
				return;
			}
			
			state = State.FIN_WAIT_2;
			
			KtnDatagram finalFin = null;
			System.out.println("CLIENT: WAITING FOR FIN!!!");
			do {
				
				finalFin = receivePacket(true);
				
			} while (!isValid(finalFin));
			
			sendAck(finalFin, false);
			state = State.CLOSED;
			System.out.println("CLIENT: Connection closed");
		}
		
		// Server side
		else {
			
			sendAck(disconnectRequest, false);
			state = State.CLOSE_WAIT;
			
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {}
			
			KtnDatagram fin = constructInternalPacket(Flag.FIN);
			KtnDatagram ack = null;
			
			do {
				try {
					state = State.CLOSE_WAIT;
					simplySendPacket(fin);
					
					state = State.LAST_ACK;
					ack = receiveAck();
				} catch (ClException e) {}
			} while (!isValid(ack));
			
			if (isValid(ack)) {
				state = State.CLOSED;
				System.out.println("SERVER: Connection closed");
			}
		}
		
	}

	@Override
	protected boolean isValid(KtnDatagram packet) {
			
		if(packet==null){
			return false;
		}
			 
			
		return (packet.getChecksum() == packet.calculateChecksum());
	}
	
	private boolean validatePacketByState(KtnDatagram packet) {
		
		if(!isValid(packet)) return false;
		
		Flag f = packet.getFlag();
		
		switch (this.state) {
		
		//case 	CLOSED: 	return false; Dekkes av default
		case	LISTEN:		return f == Flag.SYN;
		case	SYN_SENT:	return f == Flag.SYN_ACK && packet.getSrc_addr().equals(remoteAddress);
		case	SYN_RCVD:	return f == Flag.ACK && packet.getSrc_addr().equals(remoteAddress);
		case	ESTABLISHED:return f == Flag.ACK || f == Flag.FIN || f == Flag.NONE;
		
		case	FIN_WAIT_1: return f == Flag.ACK && packet.getSrc_addr().equals(remoteAddress) 
										&& packet.getSrc_port() == remotePort;
		
		case	FIN_WAIT_2: return f == Flag.FIN;
		case 	LAST_ACK:	return f == Flag.ACK;
		case	CLOSE_WAIT:	return f == Flag.FIN && packet.getSrc_addr().equals(remoteAddress) 
										&& packet.getSrc_port()==remotePort;
		
		default:			return false;
		}
	}
	
	private String getIPv4Address() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        }
        catch (UnknownHostException e) {
        	System.out.println("Host set to default!");
            return "127.0.0.1";
        }
	}
	private int newPortNumber() {
		int portNumber = -1;
		
		while(portNumber==-1 || usedPorts.containsKey(portNumber)){
			portNumber = (int)(Math.random()*9000 + 1000);
		}
		return portNumber;
	}


}
